/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package fr.onevu.gwt.uibinder.rebind;

import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import fr.onevu.gwt.uibinder.attributeparsers.FieldReferenceConverter;
import fr.onevu.gwt.uibinder.rebind.model.ImplicitCssResource;
import fr.onevu.gwt.uibinder.rebind.model.OwnerClass;
import fr.onevu.gwt.uibinder.rebind.model.OwnerField;

import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * This class handles all {@link FieldWriter} instances created for the current
 * template.
 */
public class FieldManager {

	static class FieldAndSource {
		final FieldWriter field;
		final XMLElement element;

		public FieldAndSource(FieldWriter field, XMLElement element) {
			this.field = field;
			this.element = element;
		}
	}

	private static final String GETTER_PREFIX = "get_";

	private static final String BUILDER_PREFIX = "build_";

	private static final String DUPLICATE_FIELD_ERROR = "Duplicate declaration of field %1$s.";

	private static final Comparator<FieldWriter> BUILD_DEFINITION_SORT = new Comparator<FieldWriter>() {
		public int compare(FieldWriter field1, FieldWriter field2) {
			// First get type precedence, if ties the field precedence is used.
			int precedence = field2.getFieldType().getBuildPrecedence() - field1.getFieldType().getBuildPrecedence();
			if (precedence == 0) {
				precedence = field2.getBuildPrecedence() - field1.getBuildPrecedence();
			}
			return precedence;
		}
	};

	private static final Pattern JAVA_IDENTIFIER = Pattern.compile("[\\p{L}_$][\\p{L}\\p{N}_$]*");

	public static String getFieldBuilder(String fieldName) {
		return String.format(BUILDER_PREFIX + "%s()", fieldName);
	}

	public static String getFieldGetter(String fieldName) {
		return String.format(GETTER_PREFIX + "%s()", fieldName);
	}

	public static String stripFieldGetter(String fieldName) {
		if (fieldName.startsWith(GETTER_PREFIX)) {
			return fieldName.substring(GETTER_PREFIX.length());
		}
		return fieldName;
	}

	private final TypeOracle typeOracle;

	protected final MortalLogger logger;

	/**
	 * Map of field name to FieldWriter. Note its a LinkedHashMap--we want to
	 * write these out in the order they're declared.
	 */
	private final LinkedHashMap<String, FieldWriter> fieldsMap = new LinkedHashMap<String, FieldWriter>();

	/**
	 * A stack of the fields.
	 */
	private final LinkedList<FieldAndSource> parsedFieldStack = new LinkedList<FieldAndSource>();

	private LinkedHashMap<String, FieldReference> fieldReferences = new LinkedHashMap<String, FieldReference>();

	/**
	 * Counts the number of times a getter field is called, this important to
	 * decide which strategy to take when outputing getters and builders. {@see
	 * fr.onevu.gwt.uibinder.rebind.FieldWriter#writeFieldDefinition}.
	 */
	private final Map<String, Integer> gettersCounter = new HashMap<String, Integer>();

	/**
	 * Whether to use the new strategy of generating UiBinder code.
	 */
	private final boolean useLazyWidgetBuilders;

	public FieldManager(TypeOracle typeOracle, MortalLogger logger, boolean useLazyWidgetBuilders) {
		this.typeOracle = typeOracle;
		this.logger = logger;
		this.useLazyWidgetBuilders = useLazyWidgetBuilders;
	}

	/**
	 * Converts the given field to its getter. Example: <li>myWidgetX =
	 * get_myWidgetX() <li>f_html1 = get_f_html1()
	 */
	public String convertFieldToGetter(String fieldName) {
		// could this conversion can be moved to FieldWriter?
		if (!useLazyWidgetBuilders) {
			return fieldName;
		}

		incrementFieldCounter(fieldName);
		return getFieldGetter(fieldName);
	}

	/**
	 * Prevent a field from being optimized as only being referenced once (and
	 * therefore constant for all intents). This is necessary for UiRenderer
	 * fields passed as parameters to render() calls. Those fields are modified
	 * every time a template is rendered with the parameter values.
	 */
	public void disableOptimization(String fieldName) {
		// TODO(rchandia): This hackish method should go away when the
		// UiRenderer generator gets separated from the one used for
		// UiBinder. Fields corresponding to parameters of render() will
		// not use the initialization generated by the FieldWriter.

		// Incrementing the counter twice ensures no optimization happens.
		// See AbstractFieldWriter#writeFieldDefinition()
		incrementFieldCounter(fieldName);
		incrementFieldCounter(fieldName);
	}

	public FieldReference findFieldReference(String expressionIn) {
		String expression = expressionIn;
		if (useLazyWidgetBuilders) {
			expression = stripFieldGetter(expression);
		}
		String converted = FieldReferenceConverter.expressionToPath(expression);
		return fieldReferences.get(converted);
	}

	/**
	 * Initialize with field builders the generated <b>Widgets</b> inner class.
	 * {@see fr.onevu.gwt.uibinder.rebind.FieldWriter#writeFieldBuilder}.
	 */
	public void initializeWidgetsInnerClass(IndentedWriter w, OwnerClass ownerClass) throws UnableToCompleteException {

		FieldWriter[] fields = fieldsMap.values().toArray(new FieldWriter[fieldsMap.size()]);
		Arrays.sort(fields, BUILD_DEFINITION_SORT);

		for (FieldWriter field : fields) {
			int count = getGetterCounter(field.getName());
			field.writeFieldBuilder(w, count, ownerClass.getUiField(field.getName()));
		}
	}

	/**
	 * @param fieldName
	 *          the name of the {@link FieldWriter} to find
	 * @return the {@link FieldWriter} instance indexed by fieldName or
	 *         <b>null</b> in case fieldName is not found
	 */
	public FieldWriter lookup(String fieldName) {
		return fieldsMap.get(fieldName);
	}

	/**
	 * Remove the field at the top of the {@link #parsedFieldStack}.
	 */
	public void pop() {
		parsedFieldStack.removeFirst();
	}

	/**
	 * @param source
	 *          the element this field was parsed from
	 * @param fieldWriter
	 *          the field to push on the top of the {@link #parsedFieldStack}
	 */
	public void push(XMLElement source, FieldWriter fieldWriter) {
		parsedFieldStack.addFirst(new FieldAndSource(fieldWriter, source));
	}

	/**
	 * Used to declare fields of an existing type. If your field will hold a type
	 * that is being generated, see {@link #registerFieldOfGeneratedType}.
	 * <p>
	 * When making a field we peek at the {@link #parsedFieldStack} to make sure
	 * that the field that holds the widget currently being parsed will depended
	 * upon the field being declared. This ensures, for example, that dom id
	 * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an
	 * HTMLPanel will be declared before it is.
	 * 
	 * @param fieldWriterType
	 *          the field writer type associated
	 * @param fieldType
	 *          the type of the new field
	 * @param fieldName
	 *          the name of the new field
	 * @return a new {@link FieldWriter} instance
	 * @throws UnableToCompleteException
	 *           on duplicate name
	 */
	public FieldWriter registerField(FieldWriterType fieldWriterType, JClassType fieldType, String fieldName) throws UnableToCompleteException {
		FieldWriter field = new FieldWriterOfExistingType(this, fieldWriterType, fieldType, fieldName, logger);
		return registerField(fieldName, field);
	}

	public FieldWriter registerField(JClassType fieldType, String fieldName) throws UnableToCompleteException {
		return registerField(FieldWriterType.DEFAULT, fieldType, fieldName);
	}

	public FieldWriter registerField(String type, String fieldName) throws UnableToCompleteException {
		return registerField(typeOracle.findType(type), fieldName);
	}

	/**
	 * Used to declare fields that will hold generated instances generated
	 * CssResource interfaces. If your field will hold a reference of an existing
	 * type, see {@link #registerField}. For other generated types, use
	 * {@link #registerFieldOfGeneratedType}
	 * {@link #registerFieldForGeneratedCssResource}.
	 * <p>
	 * When making a field we peek at the {@link #parsedFieldStack} to make sure
	 * that the field that holds the widget currently being parsed will depended
	 * upon the field being declared. This ensures, for example, that dom id
	 * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an
	 * HTMLPanel will be declared before it is.
	 * 
	 * @throws UnableToCompleteException
	 *           on duplicate name
	 * @return a new {@link FieldWriter} instance
	 */
	public FieldWriter registerFieldForGeneratedCssResource(ImplicitCssResource cssResource) throws UnableToCompleteException {
		FieldWriter field = new FieldWriterOfGeneratedCssResource(this, typeOracle.findType(String.class.getCanonicalName()), cssResource, logger);
		return registerField(cssResource.getName(), field);
	}

	/**
	 * Register a new field for
	 * {@link fr.onevu.gwt.uibinder.client.LazyDomElement} types. LazyDomElement
	 * fields can only be associated with html elements. Example:
	 * 
	 * <li>LazyDomElement&lt;DivElement&gt; -&gt; &lt;div&gt;</li> <li>
	 * LazyDomElement&lt;Element&gt; -&gt; &lt;div&gt;</li> <li>
	 * LazyDomElement&lt;SpanElement&gt; -&gt; &lt;span&gt;</li>
	 * 
	 * @param templateFieldType
	 *          the html type to bind, eg, SpanElement, DivElement, etc
	 * @param ownerField
	 *          the field instance
	 */
	public FieldWriter registerFieldForLazyDomElement(JClassType templateFieldType, OwnerField ownerField) throws UnableToCompleteException {
		if (ownerField == null) {
			throw new RuntimeException("Cannot register a null owner field for LazyDomElement.");
		}
		FieldWriter field = new FieldWriterOfLazyDomElement(this, templateFieldType, ownerField, logger);
		return registerField(ownerField.getName(), field);
	}

	/**
	 * Used to declare fields of a type (other than CssResource) that is to be
	 * generated. If your field will hold a reference of an existing type, see
	 * {@link #registerField}. For generated CssResources, see
	 * {@link #registerFieldForGeneratedCssResource}.
	 * <p>
	 * When making a field we peek at the {@link #parsedFieldStack} to make sure
	 * that the field that holds the widget currently being parsed will depended
	 * upon the field being declared. This ensures, for example, that dom id
	 * fields (see {@link UiBinderWriter#declareDomIdHolder()}) used by an
	 * HTMLPanel will be declared before it is.
	 * 
	 * @param assignableType
	 *          class or interface extened or implemented by this type
	 * @param typeName
	 *          the full qualified name for the class associated with the field
	 * @param fieldName
	 *          the name of the field
	 * @throws UnableToCompleteException
	 *           on duplicate name
	 * @return a new {@link FieldWriter} instance
	 */
	public FieldWriter registerFieldOfGeneratedType(JClassType assignableType, String typePackage, String typeName, String fieldName)
			throws UnableToCompleteException {
		FieldWriter field = new FieldWriterOfGeneratedType(this, assignableType, typePackage, typeName, fieldName, logger);
		return registerField(fieldName, field);
	}

	/**
	 * Called to register a <code>{field.reference}</code> encountered during
	 * parsing, to be validated against the type oracle once parsing is complete.
	 */
	public void registerFieldReference(XMLElement source, String fieldReferenceString, JType... types) {
		source = source != null ? source : parsedFieldStack.peek().element;

		FieldReference fieldReference = fieldReferences.get(fieldReferenceString);
		if (fieldReference == null) {
			fieldReference = new FieldReference(fieldReferenceString, source, this, typeOracle);
			fieldReferences.put(fieldReferenceString, fieldReference);
		}

		fieldReference.addLeftHandType(source, types);
	}

	/**
	 * Gets a FieldWriter given its name or throws a RuntimeException if not
	 * found.
	 * 
	 * @param fieldName
	 *          the name of the {@link FieldWriter} to find
	 * @return the {@link FieldWriter} instance indexed by fieldName
	 */
	public FieldWriter require(String fieldName) {
		FieldWriter fieldWriter = lookup(fieldName);
		if (fieldWriter == null) {
			throw new RuntimeException(String.format("The required field \"%s\" doesn't exist.", fieldName));
		}
		return fieldWriter;
	}

	/**
	 * To be called after parsing is complete. Surveys all
	 * <code>{field.reference}</code>s and checks they refer to existing types,
	 * and have appropriate return types.
	 * 
	 * @throws UnableToCompleteException
	 *           if any <code>{field.references}</code> can't be resolved
	 */
	public void validate() throws UnableToCompleteException {
		boolean failed = false;

		for (Map.Entry<String, FieldReference> entry : fieldReferences.entrySet()) {
			FieldReference ref = entry.getValue();
			MonitoredLogger monitoredLogger = new MonitoredLogger(logger);
			ref.validate(monitoredLogger);
			failed |= monitoredLogger.hasErrors();
		}
		if (failed) {
			throw new UnableToCompleteException();
		}
	}

	/**
	 * Outputs the getter and builder definitions for all fields. {@see
	 * fr.onevu.gwt.uibinder.rebind.AbstractFieldWriter#writeFieldDefinition}.
	 */
	public void writeFieldDefinitions(IndentedWriter writer, TypeOracle typeOracle, OwnerClass ownerClass, DesignTimeUtils designTime)
			throws UnableToCompleteException {
		Collection<FieldWriter> fields = fieldsMap.values();
		for (FieldWriter field : fields) {
			int counter = getGetterCounter(field.getName());
			// field.writeFieldDefinition(writer, typeOracle,
			// ownerClass.getUiField(field.getName()), designTime, counter,
			// useLazyWidgetBuilders);
			field.writeFieldDefinition(writer, typeOracle, ownerClass, field, designTime, counter, useLazyWidgetBuilders);
		}
	}

	/**
	 * Writes all stored gwt fields.
	 * 
	 * @param writer
	 *          the writer to output
	 */
	public void writeGwtFieldsDeclaration(IndentedWriter writer) throws UnableToCompleteException {
		Collection<FieldWriter> fields = fieldsMap.values();
		for (FieldWriter field : fields) {
			field.write(writer);
		}
	}

	private void ensureValidity(String fieldName) throws UnableToCompleteException {
		if (!JAVA_IDENTIFIER.matcher(fieldName).matches()) {
			logger.die("Illegal field name \"%s\"", fieldName);
		}
	}

	/**
	 * Gets the number of times a getter for the given field is called.
	 */
	private int getGetterCounter(String fieldName) {
		Integer count = gettersCounter.get(fieldName);
		return (count == null) ? 0 : count;
	}

	/**
	 * Increments the number of times a getter for the given field is called.
	 */
	private void incrementFieldCounter(String fieldName) {
		int count = getGetterCounter(fieldName) + 1;
		gettersCounter.put(fieldName, count);
	}

	protected FieldWriter registerField(String fieldName, FieldWriter field) throws UnableToCompleteException {
		ensureValidity(fieldName);
		requireUnique(fieldName);
		fieldsMap.put(fieldName, field);

		if (parsedFieldStack.size() > 0) {
			FieldWriter parent = parsedFieldStack.peek().field;
			field.setBuildPrecedence(parent.getBuildPrecedence() + 1);
			parent.needs(field);
		}

		return field;
	}

	private void requireUnique(String fieldName) throws UnableToCompleteException {
		if (fieldsMap.containsKey(fieldName)) {
			logger.die(DUPLICATE_FIELD_ERROR, fieldName);
		}
	}
}
